#!/usr/bin/perl -w
use strict;
use IPTables::ChainMgr;
use JSON;
use Getopt::Long;

use Data::Dumper;

my $rulesdir="/home/emathis/scripts/Firewall/firewall.rules";  #JOSN list of rules with filenamed ending in .conf
my $networksdir="/home/emathis/scripts/Firewall/networks";  #JSON list of networks ending in .conf
my @params=qw/protocol source destination jump goto/;   #Allowed iptables parameters
my @chains=qw/INPUT OUTPUT FORWARD PREROUTING POSTROUTING/; #Default chains
my @jumps=qw/ALLOW REJECT DENY LOG/;    #Allowed jumps -j

my @fwrules;
my %nets;

my %opt;
GetOptions( 
    \%opt,
    'debug',
    'start',
    'stop',
);

my $debug=0;
$debug=1 if $opt{debug};

my $ipt_bin = '/sbin/iptables'; # can set this to /sbin/ip6tables

my %opts = (
    'iptables' => $ipt_bin, # can specify 'ip6tables' hash key instead
    'iptout'   => '/tmp/iptables.out',
    'ipterr'   => '/tmp/iptables.err',
    'debug'    => $debug,
    'verbose'  => $debug,

    ### advanced options
    'ipt_alarm' => 5,  ### max seconds to wait for iptables execution.
    'ipt_exec_style' => 'waitpid',  ### can be 'waitpid',
                                      ### 'system', or 'popen'.
    'ipt_exec_sleep' => 0, ### add in time delay between execution of
                             ### iptables commands (default is 0).
);

my $ipt_obj = new IPTables::ChainMgr(%opts) or die "[*] Could not acquire IPTables::ChainMgr object\n";

#Read in the the configs
&read_configs;

print Dumper @fwrules if $debug;

#Flush out the rules
$opt{stop}=1 if $opt{start};

#Stop the firewall
if ($opt{stop}) {
    print "Stopping Firewall\n";
    my @obj;
    foreach my $rule (@fwrules) {
        foreach my $chain (keys %{$rule}) {
            next if (grep(/^$chain$/,@obj)); #Dont reprocess chains
            push(@obj,$chain);
            print "\tFlushing chain $chain\n";
            $ipt_obj->flush_chain('filter',$chain);
            
            if (!grep(/^$chain$/,@chains)) {
                print "\tDeleting Chain $chain\n";
                $ipt_obj->delete_chain('filter',$chain);
            }
            
            print "\tResetting default actions\n";
            $ipt_obj->set_chain_policy('filter',$chain,'ACCEPT');
        }
    }
}

#Start the firewall
if ($opt{start}) {
    foreach my $rule ( @fwrules) {
        add_rules($rule);
    }
}

#read in the configs
sub read_configs{
    #Read in the rules
    opendir(my $rd, $rulesdir) or die "Cant read dir $rulesdir: $!\n";
    my @rules = grep { /\.conf$/ && -f "$rulesdir/$_" } readdir($rd);
    closedir $rd;
    #Read in the network info
    opendir(my $nd, $networksdir) or die "Cant read dir $networksdir: $!\n";
    my @networks = grep { /\.conf$/ && -f "$networksdir/$_" } readdir($nd);
    closedir $nd;

    #read the networks file.
    foreach my $nfile (@networks) {
        my $file="$networksdir/$nfile";
        print "Reading in Netowrks from $nfile\n" if $debug;
        local $/;
        open( my $fh, '<', $file ) or die "Cant read network file $file $!\n";
        my $json = <$fh>;
        close $fh;
        my $ref=decode_json( $json ); #turn in to a perl hashref
        for my $alias (keys %{ $ref } ) {
            $nets{$alias}=$ref->{$alias}; #pack in the network info
        }
    }
    #print Data::Dumper->new([\%nets],[qw(Networks)])->Indent(3)->Quotekeys(0)->Dump;
    
    #read the rules files
    foreach my $rfile (@rules) {
        my $file="$rulesdir/$rfile";
        print "Reading in Rules from $rfile\n" if $debug;
        local $/;
        open( my $fh, '<', $file ) or die "Cant read rule file $file $!\n";
        my $json = <$fh>;
        close $fh;
        my $input = decode_json( $json ); #turn in to a perl hashref
        push(@fwrules, $input); #pack in the rules
    }
    return @fwrules;
    #print Dumper @fwrules; exit;
}

sub add_rules {
    my $data=shift;

    for my $chain (keys %{$data}) {
print "CHAIN: $chain\n";

        print "\tFound in \@chains\n" if $debug;
        my %rule;
            
        if ($data->{$chain}{comment}) {
            print "\tAdding rule $data->{$chain}{comment}\n";
        }
            
        if ($data->{$chain}{default}) {
            print "\tApplying default action for $chain\n";
            my ($rv, $out_ar, $errs_ar)=$ipt_obj->set_chain_policy('filter', $chain, $data->{$chain}{default});
            if ($rv) {
                print "\tSuccess!\n";
            } else {
                print "\tFAILED!\n";
                foreach my $err (@{$errs_ar}) { print "\tERROR: $err\n"; }
            }
        }
        
        #process each rule for $chain
        for my $params (keys %{ $data->{$chain} }) {
            my $param=$data->{$chain}{$params}; #ref
            if (ref($param) eq "ARRAY") { #logic for adding rules is different for arrays
                
                foreach my $val (@{ $param }) {
                    if ($params eq "source") {    
                        if ( exists $nets{$val} ) {
                            push(@{$rule{src}}, @{$nets{$val}});
                        } else {
                            push(@{$rule{src}}, $val); #store all the rules in a HoA
                        }
                        next;
                    }
                        
                    if ($params eq "destination") {
                            
                        if ( exists $nets{$val} ){
                            push( @{$rule{dst}}, @{$nets{$val}} );
                        } else {
                            push(@{$rule{dst}},$val);
                        }
                        next;
                    }
                        
                    if ($params eq "dport") {
                        push(@{$rule{dport}},$val);
                        next;
                    }
                    if ($params eq "sport") {
                        push(@{$rule{sport}},$val);
                        next;
                    }
                    if ($params eq "protocol") {
                        push(@{$rule{proto}},$val);
                        next;
                    }
                    if ($params eq "raw") { #dont store raw, go a head and process them
                        print "\tRAW: $val\n" if $debug;
                        my ($rv, $out_ar, $errs_ar) = $ipt_obj->run_ipt_cmd($val);
                        if ($rv) {
                            print "\tSuccess!\n";
                        } else {
                            print "\tERROR: $_\n" for @$errs_ar;
                        }
                        next;
                    }
                }
            }
                
            if (!ref($param)) { #not an ARRAY, but a single entry
                if ($param =~ /\*/ and ($params eq "source" or $params eq "destination") ) { #default allow all.
                    $param = "0.0.0.0/0";
                }
                if ($params eq "source") {
                    if ( exists $nets{$param} ){
                            push( @{$rule{src}},$nets{$param} );
                    } else {
                        push(@{ $rule{src}}, $param); #store all the rules in a HoA
                    }
                    next;
                }
                if ($params eq "destination") {
                    if ( exists $nets{$param} ){
                            push( @{$rule{dst}},$nets{$param} );
                    } else {
                        push(@{ $rule{dst}}, $param);
                    }
                    next;
                }
                    
                if ($params eq "dport") {
                    push(@{$rule{dport}},$param);
                    next;
                }
                    
                if ($params eq "sport") {
                    push(@{$rule{sport}},$param);
                    next;
                }
                        
                if ($params eq "protocol") {
                    push(@{$rule{proto}},$param);
                    next;
                }
                    
                if ($params eq "jump") {
                    $rule{jump}=$param;
                    next;
                }
                    
                if ($params eq "raw") {
                    my ($rv, $out_ar, $errs_ar) = $ipt_obj->run_ipt_cmd($param);
                    if ($rv) {
                        print "rules:\n";
                        print for @$errs_ar;
                    }
                    next;
                }
            }
        }
        
        $rule{chain}=$chain;
        my ($things,$opts);
        ($things,$opts)=build_rules(\%rule); # maps the src,dst stuff
        if (!$things) { print "Failed to add rule $data->{$chain}{comment}\n"; return; }
            
        #add the rules as defined by &build_rules
        for my $id (keys %{$things}) { #results of mapping,addition
            my ($rv, $out_ar, $errs_ar) = $ipt_obj->append_ip_rule( @{$things->{$id}} , $opts->{$id} );
            if ($rv) {
                print "Success!\n";
            } else {
                print "FAILED!\n";
                foreach my $err (@{$errs_ar}) { print "ERROR: $err\n"; }
            }
        }
        #print Data::Dumper->new([$things],[qw(rules)])->Indent(3)->Quotekeys(0)->Dump;
        #print Data::Dumper->new([$opts],[qw(opts)])->Indent(3)->Quotekeys(0)->Dump;    
    }
}

#figures out the logic of src,dst mappings,then adds it.
sub build_rules {
    my $rules=shift;
print Data::Dumper->new([$rules],[qw(*rules*)])->Indent(3)->Quotekeys(0)->Dump;
    my %rule;
    my %opts;
    
    #make refs 
    my $chain=$rules->{chain};
    my $jump=$rules->{jump};
    my $rulesdst=$rules->{dst};
    my $rulessrc=$rules->{src};
    
    my $addedchain=add_chain($chain); #go ahead and do the work of adding the chain
    if ($addedchain) { print $addedchain."\n"; return;}
    
    #if one list is larger then the other, some elements woudl be lost.
    #so we loop over the largest one.
    if ( ($rulessrc and $rulesdst) and scalar(@{$rulessrc}) >= scalar(@{$rulesdst}) ) {
        my $count=0;
        foreach my $src (@{$rulessrc}) {
print "SRC SORT\n\n\n";
            foreach my $dst (@{$rulesdst}) {
                push(@{$rule{$count}}, $src,$dst,'filter',$chain,$jump);
                $count++;
            }
        }
    } elsif ( ($rulessrc and $rulesdst) and scalar(@{$rulessrc}) > scalar(@{$rulesdst}) ) {
        my $count=0;
        foreach my $dst (@{$rulesdst}) {
print "DST SORT\n\n\n";
            foreach my $src ( @{$rulessrc} ) {
                push(@{$rule{$count}}, $src,$dst,'filter',$chain,$jump);
                $count++;
            }
        }
    }
    print Data::Dumper->new([\%rule],[qw(RULES)])->Indent(3)->Quotekeys(0)->Dump if $debug;
    for my $id (keys %rule) {
        foreach my $proto (@{$rules->{proto}}) {
            $opts{$id}{protocol}=$proto;
        }
        foreach my $spt (@{$rules->{sport}}) {
            $opts{$id}{s_port}=$spt;
        }
        foreach my $dpt (@{$rules->{dport}}) {
            $opts{$id}{d_port}=$dpt;
        }
        $opts{$id}{normalize}=1;
    }
    return \%rule,\%opts; #return the src/dst mappings and their options
}

#do the deed
sub add_chain {
    my $chain=shift;
    return "Missing chain" if !$chain;
    
    my ($rv, $out_ar, $errs_ar) = $ipt_obj->chain_exists('filter', $chain);
    if (!$rv) {
        print "\tAdding chain $chain\n" if $debug;
        my ($rv, $out_ar, $errs_ar) = $ipt_obj->create_chain('filter', $chain);
        return if $rv;
        return "Failed to add chain $chain $errs_ar";
    }
}